﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Xml;
using System.Xml.XPath;

namespace ComicsEditor
{
    public class TextArea : IEnumerable<TextLine>
    {
        private List<TextLine> textLines = new List<TextLine>();

        public TextArea()
        {
        }

        public TextArea(XPathNavigator nav)
        {
            TextInfo textInfo = TextInfo.Default;
            string space = (string)nav.Evaluate("string(ancestor::*[@xml:space][1]/@xml:space)", new XmlNamespaceManager(nav.NameTable));
            if (space != "")
                textInfo.preserveWS = space == "preserve";
            textInfo.LoadInfo(nav);

            XPathNodeIterator it = nav.SelectChildren("text", "");
            while (it.MoveNext())
                textLines.Add(new TextLine(textInfo, it.Current));
        }

        public void Draw(Graphics g)
        {
            g.SmoothingMode = SmoothingMode.AntiAlias;
            foreach (TextLine line in textLines)
                line.Draw(g);
        }

        public void AddLine(TextLine line)
        {
            textLines.Add(line);
        }

        public IEnumerator<TextLine> GetEnumerator()
        {
            return textLines.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }

    public class TextLine : IEnumerable<TextSpan>
    {
        private List<TextSpan> textSpans = new List<TextSpan>();
        private PointF position = PointF.Empty;
        private RectangleF box = RectangleF.Empty;

        private StringAlignment anchor = StringAlignment.Near;
        private bool vertical = false;
        private bool rightToLeft = false;

        public TextLine(StringAlignment anchor, bool vertical, bool rightToLeft)
        {
            this.anchor = anchor;
            this.vertical = vertical;
            this.rightToLeft = rightToLeft;
        }

        public TextLine(TextInfo textInfo, XPathNavigator nav)
        {
            textInfo.LoadInfo(nav);
            anchor = textInfo.anchor;
            vertical = textInfo.vertical;
            rightToLeft = textInfo.rightToLeft;

            position.X = Svg.ParseFloat(nav.GetAttribute("x", ""));
            position.Y = Svg.ParseFloat(nav.GetAttribute("y", ""));

            XPathNodeIterator it = nav.SelectChildren(XPathNodeType.All);
            while (it.MoveNext())
            {
                TextSpan span;
                if (it.Current.NodeType == XPathNodeType.Element)
                    span = new TextSpan(textInfo, it.Current, true, true);
                else
                    span = new TextSpan(textInfo, it.Current, it.CurrentPosition == 1, it.CurrentPosition == it.Count);
                AddSpan(span);
            }
        }

        public void Draw(Graphics g)
        {
            PointF pos = position;
            if (anchor == StringAlignment.Center)
                if (vertical)
                    pos.Y -= box.Height/2;
                else if (rightToLeft)
                    pos.X += box.Width/2;
                else
                    pos.X -= box.Width/2;
            if (anchor == StringAlignment.Far)
                if (vertical)
                    pos.Y -= box.Height;
                else if (rightToLeft)
                    pos.X += box.Width;
                else
                    pos.X -= box.Width;
            foreach (TextSpan span in textSpans)
            {
                span.Draw(g, pos);
                pos.X += span.InlineWidth.Width;
                pos.Y += span.InlineWidth.Height;
            }
        }

        public void AddSpan(TextSpan span)
        {
            PointF pos = PointF.Empty;
            foreach (TextSpan prevSpan in textSpans)
            {
                pos.X += prevSpan.InlineWidth.Width;
                pos.Y += prevSpan.InlineWidth.Height;
            }
            float minX = pos.X + span.Box.X;
            float minY = pos.Y + span.Box.Y;
            float maxX = minX + span.Box.Width;
            float maxY = minY + span.Box.Height;
            if (minX > box.X)
                minX = box.X;
            if (minY > box.Y)
                minY = box.Y;
            if (maxX < box.Right)
                maxX = box.Right;
            if (maxY < box.Bottom)
                maxY = box.Bottom;
            box = new RectangleF(minX, minY, maxX - minX, maxY - minY);

            textSpans.Add(span);
        }

        public PointF Position
        {
            get
            {
                return position;
            }
            set
            {
                position = value;
            }
        }

        public RectangleF Box
        {
            get
            {
                return box;
            }
        }

        public IEnumerator<TextSpan> GetEnumerator()
        {
            return textSpans.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }

    public class TextSpan
    {
        private TextInfo textInfo;
        private string text;
        private GraphicsPath path = null;
        private SizeF inlineWidth;
        private SizeF inlineHeight;
        private RectangleF box;
        private bool embedded = false;
        private string fontMapped = null;

        private static Graphics g = Graphics.FromImage(new Bitmap(1, 1));

        public TextSpan(TextInfo textInfo, string text)
        {
            this.textInfo = textInfo;
            this.text = text;
        }

        public TextSpan(TextInfo parentInfo, XPathNavigator nav, bool first, bool last)
        {
            textInfo = parentInfo;
            textInfo.LoadInfo(nav);

            text = nav.Value;
            if (textInfo.preserveWS)
                text = text.Replace("\r\n", " ").Replace("\n", " ").Replace("\t", " ");
            else
            {
                text = text.Replace("\r\n", "").Replace("\n", "").Replace("\t", " ");
                if (first)
                    text = text.TrimStart();
                if (last)
                    text = text.TrimEnd();
                Regex r = new Regex(@"\s{2,}");
                text = r.Replace(text, " ");
            }

            string svgPath = nav.GetAttribute("path", "");
            string strBox = nav.GetAttribute("box", "");
            if (svgPath != "" && strBox != "")
            {
                try
                {
                    string[] tokens = strBox.Split(',');
                    box = new RectangleF(Svg.ParseFloat(tokens[0]), Svg.ParseFloat(tokens[1]), Svg.ParseFloat(tokens[2]), Svg.ParseFloat(tokens[3]));
                    SetInlineSize(box.Size);

                    path = Svg.ParsePath(svgPath);
                    path.FillMode = FillMode.Winding;

                    embedded = true;
                }
                catch { }
            }
        }

        public void Draw(Graphics g, PointF pos)
        {
            if (path == null)
                Rebuild();

            Matrix mOld = g.Transform;
            Matrix mNew = g.Transform;
            mNew.Multiply(textInfo.transform);
            mNew.Translate(pos.X, pos.Y);
            g.Transform = mNew;
            if (textInfo.stroke != null)
                g.DrawPath(textInfo.stroke, path);
            if (textInfo.fill != null)
                g.FillPath(textInfo.fill, path);
            g.Transform = mOld;
        }

        private void Rebuild()
        {
            string fontName = textInfo.fontFamily;
            if (fontMapped != null)
                fontName = fontMapped;

            FontFamily fontFamily = null;
            switch (textInfo.fontFamily.ToLower())
            {
                case "comic":
                    fontFamily = FontInfo.Comic;
                    break;
                case "serif":
                    fontFamily = FontInfo.Serif;
                    break;
                case "sans-serif":
                    fontFamily = FontInfo.SansSerif;
                    break;
                case "monospace":
                    fontFamily = FontInfo.Monospace;
                    break;
                default:
                    try
                    {
                        foreach (FontFamily embeddedFamily in FontInfo.EmbeddedFonts.Families)
                            if (fontName == embeddedFamily.Name)
                                fontFamily = embeddedFamily;
                        if (fontFamily == null)
                            fontFamily = new FontFamily(fontName);
                    }
                    catch
                    {
                        fontFamily = FontInfo.Comic;
                    }
                    break;
            }
            Font font;
            float emSize;
            try
            {
                emSize = textInfo.fontSize*fontFamily.GetEmHeight(textInfo.fontStyle | textInfo.fontWeight)/fontFamily.GetLineSpacing(textInfo.fontStyle | textInfo.fontWeight);
                font = new Font(fontFamily, emSize, textInfo.fontStyle | textInfo.fontWeight, GraphicsUnit.Pixel);
            }
            catch
            {
                fontFamily = FontInfo.Comic;
                emSize = textInfo.fontSize*fontFamily.GetEmHeight(textInfo.fontStyle | textInfo.fontWeight)/fontFamily.GetLineSpacing(textInfo.fontStyle | textInfo.fontWeight);
                font = new Font(fontFamily, emSize, textInfo.fontStyle | textInfo.fontWeight, GraphicsUnit.Pixel);
            }
            StringFormat format = (StringFormat)StringFormat.GenericTypographic.Clone();
            format.LineAlignment = textInfo.lineAlignment;
            format.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces;
            if (textInfo.rightToLeft)
                format.FormatFlags |= StringFormatFlags.DirectionRightToLeft;
            if (textInfo.vertical)
                format.FormatFlags |= StringFormatFlags.DirectionVertical;

            SizeF size = g.MeasureString(text, font, PointF.Empty, format);
            if (size.Width == 0)
            {
                if (textInfo.vertical)
                    size.Width = textInfo.fontSize;
                else
                    size.Height = textInfo.fontSize;
            }
            SetInlineSize(size);

            PointF pos = PointF.Empty;
            if (textInfo.addDescent)
            {
                float delta = font.Size*fontFamily.GetCellDescent(font.Style)/fontFamily.GetEmHeight(font.Style);
                if (textInfo.vertical)
                    pos.X -= delta;
                else
                    pos.Y += delta;
            }

            path = new GraphicsPath();
            path.AddString(text, fontFamily, (int)font.Style, font.Size, pos, format);
            path.FillMode = FillMode.Winding;

            box.Location = pos;
            if (inlineWidth.Width < 0)
                box.X += inlineWidth.Width;
            if (textInfo.lineAlignment == StringAlignment.Near)
                box.X -= inlineHeight.Width;
            if (textInfo.lineAlignment == StringAlignment.Center)
            {
                box.X -= inlineHeight.Width/2;
                box.Y -= inlineHeight.Height/2;
            }
            if (textInfo.lineAlignment == StringAlignment.Far)
                box.Y -= inlineHeight.Height;
            box.Width = size.Width;
            box.Height = size.Height;
        }

        private void SetInlineSize(SizeF size)
        {
            inlineWidth = SizeF.Empty;
            inlineHeight = SizeF.Empty;
            if (textInfo.vertical)
            {
                inlineWidth.Height = size.Height;
                inlineHeight.Width = size.Width;
            }
            else if (textInfo.rightToLeft)
            {
                inlineWidth.Width = -size.Width;
                inlineHeight.Height = size.Height;
            }
            else
            {
                inlineWidth.Width = size.Width;
                inlineHeight.Height = size.Height;
            }
        }

        public SizeF InlineWidth
        {
            get
            {
                if (path == null)
                    Rebuild();
                return inlineWidth;
            }
        }

        public SizeF InlineHeight
        {
            get
            {
                if (path == null)
                    Rebuild();
                return inlineHeight;
            }
        }

        public RectangleF Box
        {
            get
            {
                if (path == null)
                    Rebuild();
                return box;
            }
        }

        public string FontFamily
        {
            get
            {
                return textInfo.fontFamily;
            }
        }

        public string FontName 
        {
            get
            {
                return textInfo.fontFamily
                       + (textInfo.fontWeight == FontStyle.Bold ? " Bold" : "")
                       + (textInfo.fontStyle == FontStyle.Italic ? " Italic" : "");
            }
        }

        public bool Embedded
        {
            get
            {
                return embedded;
            }
            set
            {
                if (embedded != value)
                    Rebuild();
                embedded = value;
            }
        }

        public string FontMapped
        {
            get
            {
                return fontMapped;
            }
            set
            {
                fontMapped = value;
                Rebuild();
                embedded = false;
            }
        }

        public string Path
        {
            get
            {
                if (path == null)
                    Rebuild();

                return Svg.PathToString(path);
            }
        }

        public string Text
        {
            get
            {
                return text;
            }
        }

        public TextInfo TextInfo
        {
            get
            {
                return textInfo;
            }
        }
    }

    public struct TextInfo
    {
        public bool preserveWS;
        public Brush fill;
        public Pen stroke;
        public Matrix transform;
        public string fontFamily;
        public float fontSize;
        public FontStyle fontWeight;
        public FontStyle fontStyle;
        public bool vertical;
        public bool rightToLeft;
        public StringAlignment lineAlignment;
        public bool addDescent;
        public StringAlignment anchor;

        public static TextInfo Default
        {
            get
            {
                TextInfo info;
                info.preserveWS = false;
                info.fill = null;
                info.stroke = null;
                info.transform = new Matrix();
                info.fontFamily = "comic";
                info.fontSize = 10f;
                info.fontWeight = FontStyle.Regular;
                info.fontStyle = FontStyle.Regular;
                info.vertical = false;
                info.rightToLeft = false;
                info.lineAlignment = StringAlignment.Far;
                info.addDescent = true;
                info.anchor = StringAlignment.Near;
                return info;
            }
        }

        public void LoadInfo(XPathNavigator nav)
        {
            string val;

            val = nav.GetAttribute("space", "http://www.w3.org/XML/1998/namespace");
            if (val != "")
                preserveWS = val == "preserve";

            val = nav.GetAttribute("fill", "");
            if (val != "")
                fill = Svg.ParseFill(nav);

            val = nav.GetAttribute("stroke", "");
            if (val != "")
                stroke = Svg.ParseStroke(nav);

            val = nav.GetAttribute("transform", "");
            if (val != "")
            {
                transform = transform.Clone();
                string[] tokens = nav.GetAttribute("transform", "").Replace("(", " ( ").Replace(")", " ) ").Split(new char[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries);
                Matrix m;
                for (int i = 0; i < tokens.Length; i++)
                    switch (tokens[i])
                    {
                        case "matrix":
                            if (tokens[i + 1] != "(" || tokens[i + 8] != ")")
                                break;
                            m = new Matrix(Svg.ParseFloat(tokens[i + 2]),
                                           Svg.ParseFloat(tokens[i + 3]),
                                           Svg.ParseFloat(tokens[i + 4]),
                                           Svg.ParseFloat(tokens[i + 5]),
                                           Svg.ParseFloat(tokens[i + 6]),
                                           Svg.ParseFloat(tokens[i + 7]));
                            transform.Multiply(m);
                            i += 8;
                            break;
                        case "translate":
                            if (tokens[i + 1] != "(")
                                break;
                            if (tokens[i + 3] == ")")
                            {
                                m = new Matrix();
                                m.Translate(Svg.ParseFloat(tokens[i + 2]), 0f);
                                transform.Multiply(m);
                                i += 3;
                            }
                            else if (tokens[i + 4] == ")")
                            {
                                m = new Matrix();
                                m.Translate(Svg.ParseFloat(tokens[i + 2]), Svg.ParseFloat(tokens[i + 3]));
                                transform.Multiply(m);
                                i += 4;
                            }
                            break;
                        case "scale":
                            if (tokens[i + 1] != "(")
                                break;
                            if (tokens[i + 3] == ")")
                            {
                                m = new Matrix();
                                float scale = Svg.ParseFloat(tokens[i + 2]);
                                m.Scale(scale, scale);
                                transform.Multiply(m);
                                i += 3;
                            }
                            else if (tokens[i + 4] == ")")
                            {
                                m = new Matrix();
                                m.Scale(Svg.ParseFloat(tokens[i + 2]), Svg.ParseFloat(tokens[i + 3]));
                                transform.Multiply(m);
                                i += 4;
                            }
                            break;
                        case "rotate":
                            if (tokens[i + 1] != "(")
                                break;
                            if (tokens[i + 3] == ")")
                            {
                                m = new Matrix();
                                m.Rotate(Svg.ParseFloat(tokens[i + 2]));
                                transform.Multiply(m);
                                i += 3;
                            }
                            else if (tokens[i + 5] == ")")
                            {
                                m = new Matrix();
                                m.RotateAt(Svg.ParseFloat(tokens[i + 2]),
                                           new PointF(Svg.ParseFloat(tokens[i + 3]), Svg.ParseFloat(tokens[i + 4])));
                                transform.Multiply(m);
                                i += 5;
                            }
                            break;
                        case "skewX":
                            if (tokens[i + 1] != "(" || tokens[i + 3] != ")")
                                break;
                            m = new Matrix(1, 0, (float)Math.Tan(Svg.ParseFloat(tokens[i + 2])/180*Math.PI), 1, 0, 0);
                            transform.Multiply(m);
                            i += 3;
                            break;
                        case "skewY":
                            if (tokens[i + 1] != "(" || tokens[i + 3] != ")")
                                break;
                            m = new Matrix(1, (float)Math.Tan(Svg.ParseFloat(tokens[i + 2])/180*Math.PI), 0, 1, 0, 0);
                            transform.Multiply(m);
                            i += 3;
                            break;
                    }
            }

            val = nav.GetAttribute("font-family", "");
            if (val != "")
                fontFamily = val;

            val = nav.GetAttribute("font-size", "");
            if (val != "")
                fontSize = Svg.ParseFloat(val);

            val = nav.GetAttribute("font-weight", "");
            if (val != "")
                if (val == "normal")
                    fontWeight = FontStyle.Regular;
                else if (val == "bold")
                    fontWeight = FontStyle.Bold;

            val = nav.GetAttribute("font-style", "");
            if (val != "")
                if (val == "normal")
                    fontStyle = FontStyle.Regular;
                else if (val == "italic")
                    fontStyle = FontStyle.Italic;

            val = nav.GetAttribute("writing-mode", "");
            if (val != "")
                switch (val)
                {
                    default:
                    case "lr":
                        vertical = false;
                        rightToLeft = false;
                        lineAlignment = StringAlignment.Far;
                        addDescent = true;
                        break;
                    case "rl":
                        vertical = false;
                        rightToLeft = true;
                        lineAlignment = StringAlignment.Far;
                        addDescent = true;
                        break;
                    case "tb":
                        vertical = true;
                        rightToLeft = true;
                        lineAlignment = StringAlignment.Center;
                        addDescent = false;
                        break;
                }

            val = nav.GetAttribute("glyph-orientation-vertical", "");
            if (val != "")
            {
                // Not supported.
            }

            val = nav.GetAttribute("dominant-baseline", "");
            if (val != "")
                switch (val)
                {
                    default:
                    case "alphabetic":
                        lineAlignment = StringAlignment.Far;
                        addDescent = true;
                        break;
                    case "middle":
                        lineAlignment = StringAlignment.Center;
                        addDescent = false;
                        break;
                    case "text-after-edge":
                        lineAlignment = StringAlignment.Far;
                        addDescent = false;
                        break;
                    case "text-before-edge":
                        lineAlignment = StringAlignment.Near;
                        addDescent = false;
                        break;
                }

            val = nav.GetAttribute("text-anchor", "");
            if (val != "")
                switch (val)
                {
                    default:
                    case "start":
                        anchor = StringAlignment.Near;
                        break;
                    case "middle":
                        anchor = StringAlignment.Center;
                        break;
                    case "end":
                        anchor = StringAlignment.Far;
                        break;
                }
        }
    }
}
